# SPDX-License-Identifier: Apache-2.0
"""Logging configuration for the application."""
import datetime
import json
import logging
import os
import sys
from functools import lru_cache, partial
from logging import Logger
from logging.config import dictConfig
from os import path
from types import MethodType
from typing import Any, Optional, cast

# 环境变量配置
APP_CONFIGURE_LOGGING = os.environ.get("APP_CONFIGURE_LOGGING", "1") == "1"
APP_LOGGING_CONFIG_PATH = os.environ.get("APP_LOGGING_CONFIG_PATH", "")
APP_LOGGING_LEVEL = os.environ.get("APP_LOGGING_LEVEL", "INFO")
APP_LOGGING_PREFIX = os.environ.get("APP_LOGGING_PREFIX", "[KERNEL_GEN] ")

_FORMAT = (f"{APP_LOGGING_PREFIX}%(levelname)s %(asctime)s "
           "%(filename)s:%(lineno)d] %(message)s")
_DATE_FORMAT = "%m-%d %H:%M:%S"

# 默认日志配置
DEFAULT_LOGGING_CONFIG = {
    "formatters": {
        "app": {
            "class": "logging.Formatter",
            "datefmt": _DATE_FORMAT,
            "format": _FORMAT,
        },
    },
    "handlers": {
        "app": {
            "class": "logging.StreamHandler",
            "formatter": "app",
            "level": APP_LOGGING_LEVEL,
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "app": {
            "handlers": ["app"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
    "version": 1,
    "disable_existing_loggers": False
}


class NewLineFormatter(logging.Formatter):
    """支持多行日志格式化的格式化器。"""

    def format(self, record: logging.LogRecord) -> str:
        message = super().format(record)
        # 如果消息中有换行符，保持相同的缩进
        if "\n" in message:
            parts = message.split("\n")
            subsequent_indent = " " * (len(parts[0]) - len(parts[0].lstrip()))
            return "\n".join([parts[0]] + [subsequent_indent + p for p in parts[1:]])
        return message


@lru_cache
def _print_info_once(logger: Logger, msg: str) -> None:
    # 设置stacklevel为2以打印原始调用者的行信息
    logger.info(msg, stacklevel=2)


@lru_cache
def _print_warning_once(logger: Logger, msg: str) -> None:
    # 设置stacklevel为2以打印原始调用者的行信息
    logger.warning(msg, stacklevel=2)


@lru_cache
def _print_error_once(logger: Logger, msg: str) -> None:
    # 设置stacklevel为2以打印原始调用者的行信息
    logger.error(msg, stacklevel=2)


class _AppLogger(Logger):
    """
    注意:
        此类仅用于提供类型信息。
        我们实际上直接在 :class:`logging.Logger` 实例上修补方法，
        以避免与其他库冲突。
    """

    def info_once(self, msg: str) -> None:
        """
        与 :meth:`info` 相同，但后续使用相同消息的调用将被静默丢弃。
        """
        _print_info_once(self, msg)

    def warning_once(self, msg: str) -> None:
        """
        与 :meth:`warning` 相同，但后续使用相同消息的调用将被静默丢弃。
        """
        _print_warning_once(self, msg)
        
    def error_once(self, msg: str) -> None:
        """
        与 :meth:`error` 相同，但后续使用相同消息的调用将被静默丢弃。
        """
        _print_error_once(self, msg)


def _configure_app_root_logger() -> None:
    """配置根日志记录器"""
    logging_config = dict[str, Any]()

    if not APP_CONFIGURE_LOGGING and APP_LOGGING_CONFIG_PATH:
        raise RuntimeError(
            "APP_CONFIGURE_LOGGING 设置为 false，但提供了 "
            "APP_LOGGING_CONFIG_PATH。APP_LOGGING_CONFIG_PATH "
            "意味着启用 APP_CONFIGURE_LOGGING。请启用 "
            "APP_CONFIGURE_LOGGING 或取消设置 APP_LOGGING_CONFIG_PATH。")

    if APP_CONFIGURE_LOGGING:
        logging_config = DEFAULT_LOGGING_CONFIG

    if APP_LOGGING_CONFIG_PATH:
        if not path.exists(APP_LOGGING_CONFIG_PATH):
            raise RuntimeError(
                "无法加载日志配置。文件不存在: %s",
                APP_LOGGING_CONFIG_PATH)
        with open(APP_LOGGING_CONFIG_PATH, encoding="utf-8") as file:
            custom_config = json.loads(file.read())

        if not isinstance(custom_config, dict):
            raise ValueError("无效的日志配置。期望Dict，得到%s。",
                             type(custom_config).__name__)
        logging_config = custom_config

    # 更新NewLineFormatter类路径
    for formatter in logging_config.get("formatters", {}).values():
        if formatter.get("class", "").endswith("NewLineFormatter"):
            formatter["class"] = "NewLineFormatter"

    if logging_config:
        # # 将NewLineFormatter类添加到logging模块
        # logging.NewLineFormatter = NewLineFormatter
        dictConfig(logging_config)


def init_logger(name: str) -> _AppLogger:
    """
    初始化并获取日志记录器。
    
    此函数的主要目的是确保以这样的方式检索日志记录器，
    以便我们可以确定根app记录器已经配置好了。
    
    Args:
        name: 日志记录器名称，通常是模块名称
        
    Returns:
        配置好的日志记录器实例
    """
    # 确保根日志记录器已经配置
    if name.startswith("app."):
        logger = logging.getLogger(name)
    else:
        logger = logging.getLogger(f"app.{name}")

    # 添加额外方法
    methods_to_patch = {
        "info_once": _print_info_once,
        "warning_once": _print_warning_once,
        "error_once": _print_error_once,
    }

    for method_name, method in methods_to_patch.items():
        setattr(logger, method_name, MethodType(partial(method, logger), logger))

    return cast(_AppLogger, logger)


def add_file_handler(log_file_path: str, level: str = "INFO"):
    """添加文件处理器到根日志记录器"""
    root_logger = logging.getLogger("app")
    file_handler = logging.FileHandler(log_file_path)
    file_handler.setLevel(getattr(logging, level))
    formatter = logging.Formatter(_FORMAT, datefmt=_DATE_FORMAT)
    file_handler.setFormatter(formatter)
    root_logger.addHandler(file_handler)
    return file_handler

def switch_handler(log_file_path,old_handler,level: str = "INFO"):
    root_logger = logging.getLogger("app")
    root_logger.removeHandler(old_handler)
    return add_file_handler(log_file_path,level)
    

def _trace_calls(log_path, root_dir, frame, event, arg=None):
    """函数调用追踪器"""
    if event in ['call', 'return']:
        # 提取文件名、行号、函数名和代码对象
        filename = frame.f_code.co_filename
        lineno = frame.f_lineno
        func_name = frame.f_code.co_name
        if not filename.startswith(root_dir):
            # 只记录root_dir中的函数
            return
        # 记录每个函数调用或返回
        try:
            last_frame = frame.f_back
            if last_frame is not None:
                last_filename = last_frame.f_code.co_filename
                last_lineno = last_frame.f_lineno
                last_func_name = last_frame.f_code.co_name
            else:
                # 初始帧
                last_filename = ""
                last_lineno = 0
                last_func_name = ""
            with open(log_path, 'a') as f:
                ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
                if event == 'call':
                    f.write(f"{ts} Call to"
                            f" {func_name} in {filename}:{lineno}"
                            f" from {last_func_name} in {last_filename}:"
                            f"{last_lineno}\n")
                else:
                    f.write(f"{ts} Return from"
                            f" {func_name} in {filename}:{lineno}"
                            f" to {last_func_name} in {last_filename}:"
                            f"{last_lineno}\n")
        except NameError:
            # 模块在关闭期间被删除
            pass
    return partial(_trace_calls, log_path, root_dir)


def enable_trace_function_call(log_file_path: str,
                              root_dir: Optional[str] = None):
    """
    启用对root_dir下每个函数调用的跟踪。
    这对调试挂起或崩溃很有用。
    `log_file_path`是日志文件的路径。
    `root_dir`是要跟踪的代码的根目录。如果为None，它是应用程序根目录。

    注意，此调用是线程级别的，任何调用此函数的线程都将启用跟踪。其他线程不会受到影响。
    """
    logger = init_logger(__name__)
    logger.warning(
        "函数调用跟踪已启用。它将记录Python执行的每个"
        "函数。这将减慢代码运行速度。建议"
        "仅用于调试挂起或崩溃。")
    logger.info("跟踪帧日志保存到 %s", log_file_path)
    if root_dir is None:
        # 默认情况下，这是应用程序的根目录
        root_dir = os.path.dirname(os.path.dirname(__file__))
    sys.settrace(partial(_trace_calls, log_file_path, root_dir))


# 模块导入时初始化根日志记录器。
# 这是线程安全的，因为模块只导入一次，
# 由Python GIL保证。
_configure_app_root_logger()

# 当前模块的日志记录器
logger = init_logger(__name__)